/*-------------------------------------------------------------------------
 *
 * heapam_handler.c
 *	  堆表访问方法代码
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/access/heap/heapam_handler.c
 *
 *
 * NOTES
 *	  本文件将较低级的heapam.c等例程与tableam抽象连接起来。
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/genam.h"
#include "access/heapam.h"
#include "access/heaptoast.h"
#include "access/multixact.h"
#include "access/rewriteheap.h"
#include "access/syncscan.h"
#include "access/tableam.h"
#include "access/tsmapi.h"
#include "access/xact.h"
#include "catalog/catalog.h"
#include "catalog/index.h"
#include "catalog/storage.h"
#include "catalog/storage_xlog.h"
#include "commands/progress.h"
#include "executor/executor.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "storage/bufmgr.h"
#include "storage/bufpage.h"
#include "storage/lmgr.h"
#include "storage/predicate.h"
#include "storage/procarray.h"
#include "storage/smgr.h"
#include "utils/builtins.h"
#include "utils/rel.h"

static void fc_reform_and_rewrite_tuple(HeapTuple fc_tuple,
									 Relation fc_OldHeap, Relation fc_NewHeap,
									 Datum *fc_values, bool *fc_isnull, RewriteState fc_rwstate);

static bool fc_SampleHeapTupleVisible(TableScanDesc fc_scan, Buffer fc_buffer,
								   HeapTuple fc_tuple,
								   OffsetNumber fc_tupoffset);

static BlockNumber fc_heapam_scan_get_blocks_done(HeapScanDesc fc_hscan);

static const TableAmRoutine heapam_methods;


/* ------------------------------------------------------------------------
 * 与堆AM相关的槽回调
 * ------------------------------------------------------------------------
 */

static const TupleTableSlotOps *
fc_heapam_slot_callbacks(Relation fc_relation)
{
	return &TTSOpsBufferHeapTuple;
}


/* ------------------------------------------------------------------------
 * 针对堆AM的索引扫描回调
 * ------------------------------------------------------------------------
 */

static IndexFetchTableData *
fc_heapam_index_fetch_begin(Relation fc_rel)
{
	IndexFetchHeapData *fc_hscan = palloc0(sizeof(IndexFetchHeapData));

	fc_hscan->xs_base.rel = fc_rel;
	fc_hscan->xs_cbuf = InvalidBuffer;

	return &fc_hscan->xs_base;
}

static void fc_heapam_index_fetch_reset(IndexFetchTableData *fc_scan)
{
	IndexFetchHeapData *fc_hscan = (IndexFetchHeapData *) fc_scan;

	if (BufferIsValid(fc_hscan->xs_cbuf))
	{
		ReleaseBuffer(fc_hscan->xs_cbuf);
		fc_hscan->xs_cbuf = InvalidBuffer;
	}
}

static void fc_heapam_index_fetch_end(IndexFetchTableData *fc_scan)
{
	IndexFetchHeapData *fc_hscan = (IndexFetchHeapData *) fc_scan;

	fc_heapam_index_fetch_reset(fc_scan);

	pfree(fc_hscan);
}

static bool fc_heapam_index_fetch_tuple(struct IndexFetchTableData *fc_scan,
						 ItemPointer fc_tid,
						 Snapshot fc_snapshot,
						 TupleTableSlot *fc_slot,
						 bool *fc_call_again, bool *fc_all_dead)
{
	IndexFetchHeapData *fc_hscan = (IndexFetchHeapData *) fc_scan;
	BufferHeapTupleTableSlot *fc_bslot = (BufferHeapTupleTableSlot *) fc_slot;
	bool		fc_got_heap_tuple;

	Assert(TTS_IS_BUFFERTUPLE(fc_slot));

	/* 如果我们在中间的HOT链中，可以跳过缓冲区切换逻辑。 */
	if (!*fc_call_again)
	{
		/* 如果我们还没有正确的缓冲区，则切换到正确的缓冲区 */
		Buffer		fc_prev_buf = fc_hscan->xs_cbuf;

		fc_hscan->xs_cbuf = ReleaseAndReadBuffer(fc_hscan->xs_cbuf,
											  fc_hscan->xs_base.rel,
											  ItemPointerGetBlockNumber(fc_tid));

		/*
		 * 修剪页面，但仅当我们尚未在此页面上时
		 */
		if (fc_prev_buf != fc_hscan->xs_cbuf)
			heap_page_prune_opt(fc_hscan->xs_base.rel, fc_hscan->xs_cbuf);
	}

	/* 在缓冲区上获取共享锁，以便我们可以检查可见性 */
	LockBuffer(fc_hscan->xs_cbuf, BUFFER_LOCK_SHARE);
	fc_got_heap_tuple = heap_hot_search_buffer(fc_tid,
											fc_hscan->xs_base.rel,
											fc_hscan->xs_cbuf,
											fc_snapshot,
											&fc_bslot->base.tupdata,
											fc_all_dead,
											!*fc_call_again);
	fc_bslot->base.tupdata.t_self = *fc_tid;
	LockBuffer(fc_hscan->xs_cbuf, BUFFER_LOCK_UNLOCK);

	if (fc_got_heap_tuple)
	{
		/*
		 * 仅在非MVCC快照中，HOT链的多个成员可以是可见的。
		 */
		*fc_call_again = !IsMVCCSnapshot(fc_snapshot);

		fc_slot->tts_tableOid = RelationGetRelid(fc_scan->rel);
		ExecStoreBufferHeapTuple(&fc_bslot->base.tupdata, fc_slot, fc_hscan->xs_cbuf);
	}
	else
	{
		/* 我们已到达HOT链的末端。 */
		*fc_call_again = false;
	}

	return fc_got_heap_tuple;
}


/* ------------------------------------------------------------------------
 * 针对堆AM的对单个元组执行非修改操作的回调
 * ------------------------------------------------------------------------
 */

static bool fc_heapam_fetch_row_version(Relation fc_relation,
						 ItemPointer fc_tid,
						 Snapshot fc_snapshot,
						 TupleTableSlot *fc_slot)
{
	BufferHeapTupleTableSlot *fc_bslot = (BufferHeapTupleTableSlot *) fc_slot;
	Buffer		fc_buffer;

	Assert(TTS_IS_BUFFERTUPLE(fc_slot));

	fc_bslot->base.tupdata.t_self = *fc_tid;
	if (heap_fetch(fc_relation, fc_snapshot, &fc_bslot->base.tupdata, &fc_buffer, false))
	{
		/* 存储在槽中，传递现有的固定 */
		ExecStorePinnedBufferHeapTuple(&fc_bslot->base.tupdata, fc_slot, fc_buffer);
		fc_slot->tts_tableOid = RelationGetRelid(fc_relation);

		return true;
	}

	return false;
}

static bool fc_heapam_tuple_tid_valid(TableScanDesc fc_scan, ItemPointer fc_tid)
{
	HeapScanDesc fc_hscan = (HeapScanDesc) fc_scan;

	return ItemPointerIsValid(fc_tid) &&
		ItemPointerGetBlockNumber(fc_tid) < fc_hscan->rs_nblocks;
}

static bool fc_heapam_tuple_satisfies_snapshot(Relation fc_rel, TupleTableSlot *fc_slot,
								Snapshot fc_snapshot)
{
	BufferHeapTupleTableSlot *fc_bslot = (BufferHeapTupleTableSlot *) fc_slot;
	bool		fc_res;

	Assert(TTS_IS_BUFFERTUPLE(fc_slot));
	Assert(BufferIsValid(fc_bslot->buffer));

	/*
	 * 我们需要缓冲区固定和锁以调用HeapTupleSatisfiesVisibility。
	 * 调用者应持有固定，但不应持有锁。
	 */
	LockBuffer(fc_bslot->buffer, BUFFER_LOCK_SHARE);
	fc_res = HeapTupleSatisfiesVisibility(fc_bslot->base.tuple, fc_snapshot,
									   fc_bslot->buffer);
	LockBuffer(fc_bslot->buffer, BUFFER_LOCK_UNLOCK);

	return fc_res;
}


/* ----------------------------------------------------------------------------
 * 针对堆AM的物理元组操作的函数。
 * ----------------------------------------------------------------------------
 */

static void fc_heapam_tuple_insert(Relation fc_relation, TupleTableSlot *fc_slot, CommandId fc_cid,
					int fc_options, BulkInsertState fc_bistate)
{
	bool		fc_shouldFree = true;
	HeapTuple	fc_tuple = ExecFetchSlotHeapTuple(fc_slot, true, &fc_shouldFree);

	/* 使用表OID更新元组 */
	fc_slot->tts_tableOid = RelationGetRelid(fc_relation);
	fc_tuple->t_tableOid = fc_slot->tts_tableOid;

	/* 执行插入，并复制结果ItemPointer */
	heap_insert(fc_relation, fc_tuple, fc_cid, fc_options, fc_bistate);
	ItemPointerCopy(&fc_tuple->t_self, &fc_slot->tts_tid);

	if (fc_shouldFree)
		pfree(fc_tuple);
}

static void fc_heapam_tuple_insert_speculative(Relation fc_relation, TupleTableSlot *fc_slot,
								CommandId fc_cid, int fc_options,
								BulkInsertState fc_bistate, uint32 fc_specToken)
{
	bool		fc_shouldFree = true;
	HeapTuple	fc_tuple = ExecFetchSlotHeapTuple(fc_slot, true, &fc_shouldFree);

	/* 使用表OID更新元组 */
	fc_slot->tts_tableOid = RelationGetRelid(fc_relation);
	fc_tuple->t_tableOid = fc_slot->tts_tableOid;

	HeapTupleHeaderSetSpeculativeToken(fc_tuple->t_data, fc_specToken);
	fc_options |= HEAP_INSERT_SPECULATIVE;

	/* 执行插入，并复制结果ItemPointer */
	heap_insert(fc_relation, fc_tuple, fc_cid, fc_options, fc_bistate);
	ItemPointerCopy(&fc_tuple->t_self, &fc_slot->tts_tid);

	if (fc_shouldFree)
		pfree(fc_tuple);
}

static void fc_heapam_tuple_complete_speculative(Relation fc_relation, TupleTableSlot *fc_slot,
								  uint32 fc_specToken, bool fc_succeeded)
{
	bool		fc_shouldFree = true;
	HeapTuple	fc_tuple = ExecFetchSlotHeapTuple(fc_slot, true, &fc_shouldFree);

	/* 相应地调整元组的状态 */
	if (fc_succeeded)
		heap_finish_speculative(fc_relation, &fc_slot->tts_tid);
	else
		heap_abort_speculative(fc_relation, &fc_slot->tts_tid);

	if (fc_shouldFree)
		pfree(fc_tuple);
}

static TM_Result fc_heapam_tuple_delete(Relation fc_relation, ItemPointer fc_tid, CommandId fc_cid,
					Snapshot fc_snapshot, Snapshot fc_crosscheck, bool fc_wait,
					TM_FailureData *fc_tmfd, bool fc_changingPart)
{
	/*
	 * 目前，索引元组的删除在清理时处理，如果
	 * 存储本身正在清理死元组，这是
	 * 调用索引元组删除的时机。
	 */
	return heap_delete(fc_relation, fc_tid, fc_cid, fc_crosscheck, fc_wait, fc_tmfd, fc_changingPart);
}


static TM_Result fc_heapam_tuple_update(Relation fc_relation, ItemPointer fc_otid, TupleTableSlot *fc_slot,
					CommandId fc_cid, Snapshot fc_snapshot, Snapshot fc_crosscheck,
					bool fc_wait, TM_FailureData *fc_tmfd,
					LockTupleMode *fc_lockmode, bool *fc_update_indexes)
{
	bool		fc_shouldFree = true;
	HeapTuple	fc_tuple = ExecFetchSlotHeapTuple(fc_slot, true, &fc_shouldFree);
	TM_Result	fc_result;

	/* 使用表OID更新元组 */
	fc_slot->tts_tableOid = RelationGetRelid(fc_relation);
	fc_tuple->t_tableOid = fc_slot->tts_tableOid;

	fc_result = heap_update(fc_relation, fc_otid, fc_tuple, fc_cid, fc_crosscheck, fc_wait,
						 fc_tmfd, fc_lockmode);
	ItemPointerCopy(&fc_tuple->t_self, &fc_slot->tts_tid);

	/*
	 * 决定是否需要为元组生成新的索引条目
	 *
	 * 注意：heap_update在
	 * t_self字段中返回新元组的tid（位置）。
	 *
	 * 如果这是一个HOT更新，我们绝不能插入新的索引条目。
	 */
	*fc_update_indexes = fc_result == TM_Ok && !HeapTupleIsHeapOnly(fc_tuple);

	if (fc_shouldFree)
		pfree(fc_tuple);

	return fc_result;
}

static TM_Result fc_heapam_tuple_lock(Relation fc_relation, ItemPointer fc_tid, Snapshot fc_snapshot,
				  TupleTableSlot *fc_slot, CommandId fc_cid, LockTupleMode fc_mode,
				  LockWaitPolicy fc_wait_policy, uint8 fc_flags,
				  TM_FailureData *fc_tmfd)
{
	BufferHeapTupleTableSlot *fc_bslot = (BufferHeapTupleTableSlot *) fc_slot;
	TM_Result	fc_result;
	Buffer		fc_buffer;
	HeapTuple	fc_tuple = &fc_bslot->base.tupdata;
	bool		fc_follow_updates;

	fc_follow_updates = (fc_flags & TUPLE_LOCK_FLAG_LOCK_UPDATE_IN_PROGRESS) != 0;
	fc_tmfd->traversed = false;

	Assert(TTS_IS_BUFFERTUPLE(fc_slot));

tuple_lock_retry:
	fc_tuple->t_self = *fc_tid;
	fc_result = heap_lock_tuple(fc_relation, fc_tuple, fc_cid, fc_mode, fc_wait_policy,
							 fc_follow_updates, &fc_buffer, fc_tmfd);

	if (fc_result == TM_Updated &&
		(fc_flags & TUPLE_LOCK_FLAG_FIND_LAST_VERSION))
	{
		/* 在重新检查时不应遇到投机性元组 */
		Assert(!HeapTupleHeaderIsSpeculative(fc_tuple->t_data));

		ReleaseBuffer(fc_buffer);

		if (!ItemPointerEquals(&fc_tmfd->ctid, &fc_tuple->t_self))
		{
			SnapshotData fc_SnapshotDirty;
			TransactionId fc_priorXmax;

			/* 它已被更新，因此查看更新版本 */
			*fc_tid = fc_tmfd->ctid;
			/* 更新的行应具有与此xmax匹配的xmin */
			fc_priorXmax = fc_tmfd->xmax;

			/* 发信号表示链中稍后一个元组正在被锁定 */
			fc_tmfd->traversed = true;

			/*
			 * 获取目标元组
			 *
			 * 在这里循环以处理更新或忙碌的元组
			 */
			InitDirtySnapshot(fc_SnapshotDirty);
			for (;;)
			{
				if (ItemPointerIndicatesMovedPartitions(fc_tid))
					ereport(ERROR,
							(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
							 errmsg("tuple to be locked was already moved to another partition due to concurrent update")));

				fc_tuple->t_self = *fc_tid;
				if (heap_fetch(fc_relation, &fc_SnapshotDirty, fc_tuple, &fc_buffer, true))
				{
					/*
					 * 如果xmin不是我们预期的，那么该槽位一定是被回收并用于一个无关的元组。这
					 * 意味着该行的最新版本已被删除，因此我们无需做任何操作。 (应该安全地检查xmin
					 * 而无需获取缓冲区内容锁。我们假设读取TransactionId是原子的，而且在现有元组中xmin
					 * 永远不会改变，除了无效或冻结，且这两者都无法匹配priorXmax。)
					 */
					if (!TransactionIdEquals(HeapTupleHeaderGetXmin(fc_tuple->t_data),
											 fc_priorXmax))
					{
						ReleaseBuffer(fc_buffer);
						return TM_Deleted;
					}

					/* 否则xmin不应该脏... */
					if (TransactionIdIsValid(fc_SnapshotDirty.xmin))
						ereport(ERROR,
								(errcode(ERRCODE_DATA_CORRUPTED),
								 errmsg_internal("t_xmin %u is uncommitted in tuple (%u,%u) to be updated in table \"%s\"",
												 fc_SnapshotDirty.xmin,
												 ItemPointerGetBlockNumber(&fc_tuple->t_self),
												 ItemPointerGetOffsetNumber(&fc_tuple->t_self),
												 RelationGetRelationName(fc_relation))));

					/*
					 * 如果元组正被其他事务更新，那么我们必须等待其提交/中止，或者尝试死去。
					 */
					if (TransactionIdIsValid(fc_SnapshotDirty.xmax))
					{
						ReleaseBuffer(fc_buffer);
						switch (fc_wait_policy)
						{
							case LockWaitBlock:
								XactLockTableWait(fc_SnapshotDirty.xmax,
												  fc_relation, &fc_tuple->t_self,
												  XLTW_FetchUpdated);
								break;
							case LockWaitSkip:
								if (!ConditionalXactLockTableWait(fc_SnapshotDirty.xmax))
									/* 跳过而不是等待 */
									return TM_WouldBlock;
								break;
							case LockWaitError:
								if (!ConditionalXactLockTableWait(fc_SnapshotDirty.xmax))
									ereport(ERROR,
											(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
											 errmsg("could not obtain lock on row in relation \"%s\"",
													RelationGetRelationName(fc_relation))));
								break;
						}
						continue;	/* 循环回去重复heap_fetch */
					}

					/*
					 * 如果元组是由我们自己的事务插入的，我们必须检查cmin与cid的关系：cmin >=
					 * 当前CID意味着我们的命令无法看到该元组，因此我们应该忽略它。否则heap_lock_tuple()
					 * 将抛出错误，并且任何后续尝试更新或删除该元组也会如此。 (我们无需检查cmax，因为
					 * HeapTupleSatisfiesDirty会将我们事务删除的元组视为已删除，无论cmax如何。)我们
					 * 刚刚检查了priorXmax == xmin，因此我们可以测试该变量，而不是再次进行HeapTupleHeaderGetXmin。
					 */
					if (TransactionIdIsCurrentTransactionId(fc_priorXmax) &&
						HeapTupleHeaderGetCmin(fc_tuple->t_data) >= fc_cid)
					{
						fc_tmfd->xmax = fc_priorXmax;

						/*
						 * Cmin是问题值，因此存储它。见上文。
						 */
						fc_tmfd->cmax = HeapTupleHeaderGetCmin(fc_tuple->t_data);
						ReleaseBuffer(fc_buffer);
						return TM_SelfModified;
					}

					/*
					 * 这是一个活动元组，因此再次尝试锁定它。
					 */
					ReleaseBuffer(fc_buffer);
					goto tuple_lock_retry;
				}

				/*
				 * 如果引用的槽实际上是空的，则该行的最新版本一定已被删除，因此我们无需
				 * 做任何操作。
				 */
				if (fc_tuple->t_data == NULL)
				{
					Assert(!BufferIsValid(fc_buffer));
					return TM_Deleted;
				}

				/*
				 * 如上所述，如果xmin不是我们预期的，什么都不做。
				 */
				if (!TransactionIdEquals(HeapTupleHeaderGetXmin(fc_tuple->t_data),
										 fc_priorXmax))
				{
					ReleaseBuffer(fc_buffer);
					return TM_Deleted;
				}

				/*
				 * 如果我们到这里，说明找到了元组但未通过SnapshotDirty。假设xmin是已提交的xact
				 * 或我们的xact（因为如果我们试图修改元组，它当然应该是这样的），这必须意味着该行
				 * 是被已提交的xact或我们自己的xact更新或删除的。如果是被删除，我们可以忽略它；如果是
				 * 被更新，则链接到下一个版本并重复整个过程。
				 *
				 * 如上所述，检查xmax和t_ctid应该是安全的，而无需缓冲区内容锁，因为它们不会在
				 * 改变。不过我们最好保持一个缓冲区针。
				 */
				if (ItemPointerEquals(&fc_tuple->t_self, &fc_tuple->t_data->t_ctid))
				{
					/* 被删除，因此忘掉它 */
					ReleaseBuffer(fc_buffer);
					return TM_Deleted;
				}

				/* 被更新，因此查看更新后的行 */
				*fc_tid = fc_tuple->t_data->t_ctid;
				/* 更新的行应具有与此xmax匹配的xmin */
				fc_priorXmax = HeapTupleHeaderGetUpdateXid(fc_tuple->t_data);
				ReleaseBuffer(fc_buffer);
				/* 循环回去获取链中的下一个 */
			}
		}
		else
		{
			/* 元组已被删除，因此放弃 */
			return TM_Deleted;
		}
	}

	fc_slot->tts_tableOid = RelationGetRelid(fc_relation);
	fc_tuple->t_tableOid = fc_slot->tts_tableOid;

	/* 存储在槽中，传递现有的固定 */
	ExecStorePinnedBufferHeapTuple(fc_tuple, fc_slot, fc_buffer);

	return fc_result;
}


/* ------------------------------------------------------------------------
 * 与DDL相关的heap AM回调。
 * ------------------------------------------------------------------------
 */

static void fc_heapam_relation_set_new_filenode(Relation fc_rel,
								 const RelFileNode *fc_newrnode,
								 char fc_persistence,
								 TransactionId *fc_freezeXid,
								 MultiXactId *fc_minmulti)
{
	SMgrRelation fc_srel;

	/*
	 * 初始化为可以将元组放入表中的最小 XID。我们知道，所有比 RecentXmin 更老的事务都已不再运行，因此这就足够了。
	 */
	*fc_freezeXid = RecentXmin;

	/*
	 * 同样，初始化最小 Multixact 为可以在表中的元组中存储的第一个值。正在运行的事务可能会重用来自其本地缓存的值，因此我们需要小心地考虑所有当前正在运行的多事务。
	 *
	 * XXX 这可以进一步完善，但是否值得麻烦？
	 */
	*fc_minmulti = GetOldestMultiXactId();

	fc_srel = RelationCreateStorage(*fc_newrnode, fc_persistence, true);

	/*
	 * 如果需要，为未记录的表设置初始化文件，使其能够在重启时正确重新初始化。即使页面已被记录，仍需要立即同步，因为写入并没有通过 shared_buffers，因此并发检查点可能已经将重做指针移动到我们的 xlog 记录之后。例如，在重放时，恢复可能会将此移除，如 XLOG_DBASE_CREATE* 或 XLOG_TBLSPC_CREATE 记录。因此，即使 wal_level=minimal，记录也是必要的。
	 */
	if (fc_persistence == RELPERSISTENCE_UNLOGGED)
	{
		Assert(fc_rel->rd_rel->relkind == RELKIND_RELATION ||
			   fc_rel->rd_rel->relkind == RELKIND_MATVIEW ||
			   fc_rel->rd_rel->relkind == RELKIND_TOASTVALUE);
		smgrcreate(fc_srel, INIT_FORKNUM, false);
		log_smgrcreate(fc_newrnode, INIT_FORKNUM);
		smgrimmedsync(fc_srel, INIT_FORKNUM);
	}

	smgrclose(fc_srel);
}

static void fc_heapam_relation_nontransactional_truncate(Relation fc_rel)
{
	RelationTruncate(fc_rel, 0);
}

static void fc_heapam_relation_copy_data(Relation fc_rel, const RelFileNode *fc_newrnode)
{
	SMgrRelation fc_dstrel;

	fc_dstrel = smgropen(*fc_newrnode, fc_rel->rd_backend);

	/*
	 * 因为我们直接复制文件而不查看共享缓冲区，
	 * 我们最好首先刷新源关系在共享缓冲区中的任何页。我们假设
	 * 在我们持有该关系的专有锁时不会做任何新更改。
	 */
	FlushRelationBuffers(fc_rel);

	/*
	 * 创建和复制关系的所有文件，并计划 unlink 旧的物理文件。
	 *
	 * 注意：任何在 relfilenode 值上的冲突将在
	 * RelationCreateStorage() 中被捕获。
	 */
	RelationCreateStorage(*fc_newrnode, fc_rel->rd_rel->relpersistence, true);

	/* 复制主文件 */
	RelationCopyStorage(RelationGetSmgr(fc_rel), fc_dstrel, MAIN_FORKNUM,
						fc_rel->rd_rel->relpersistence);

	/* 复制存在的额外文件 */
	for (ForkNumber fc_forkNum = MAIN_FORKNUM + 1;
		 fc_forkNum <= MAX_FORKNUM; fc_forkNum++)
	{
		if (smgrexists(RelationGetSmgr(fc_rel), fc_forkNum))
		{
			smgrcreate(fc_dstrel, fc_forkNum, false);

			/*
			 * 如果关系是持久的，或者这是未记录关系的初始化文件，
			 * 则创建 WAL 日志。
			 */
			if (RelationIsPermanent(fc_rel) ||
				(fc_rel->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED &&
				 fc_forkNum == INIT_FORKNUM))
				log_smgrcreate(fc_newrnode, fc_forkNum);
			RelationCopyStorage(RelationGetSmgr(fc_rel), fc_dstrel, fc_forkNum,
								fc_rel->rd_rel->relpersistence);
		}
	}


	/* 删除旧关系，关闭新关系 */
	RelationDropStorage(fc_rel);
	smgrclose(fc_dstrel);
}

static void fc_heapam_relation_copy_for_cluster(Relation fc_OldHeap, Relation fc_NewHeap,
								 Relation fc_OldIndex, bool fc_use_sort,
								 TransactionId fc_OldestXmin,
								 TransactionId *fc_xid_cutoff,
								 MultiXactId *fc_multi_cutoff,
								 double *fc_num_tuples,
								 double *fc_tups_vacuumed,
								 double *fc_tups_recently_dead)
{
	RewriteState fc_rwstate;
	IndexScanDesc fc_indexScan;
	TableScanDesc fc_tableScan;
	HeapScanDesc fc_heapScan;
	bool		fc_is_system_catalog;
	Tuplesortstate *fc_tuplesort;
	TupleDesc	fc_oldTupDesc = RelationGetDescr(fc_OldHeap);
	TupleDesc	fc_newTupDesc = RelationGetDescr(fc_NewHeap);
	TupleTableSlot *fc_slot;
	int			fc_natts;
	Datum	   *fc_values;
	bool	   *fc_isnull;
	BufferHeapTupleTableSlot *fc_hslot;
	BlockNumber fc_prev_cblock = InvalidBlockNumber;

	/* 记住它是否是系统目录 */
	fc_is_system_catalog = IsSystemRelation(fc_OldHeap);

	/*
	 * 有效的 smgr_targblock 意味着已经有写入操作发生在该关系上。这可能是无害的，但此函数并没有为此做计划。
	 */
	Assert(RelationGetTargetBlock(fc_NewHeap) == InvalidBlockNumber);

	/* 预分配值 / isnull 数组 */
	fc_natts = fc_newTupDesc->natts;
	fc_values = (Datum *) palloc(fc_natts * sizeof(Datum));
	fc_isnull = (bool *) palloc(fc_natts * sizeof(bool));

	/* 初始化重写操作 */
	fc_rwstate = begin_heap_rewrite(fc_OldHeap, fc_NewHeap, fc_OldestXmin, *fc_xid_cutoff,
								 *fc_multi_cutoff);


	/* 如果需要，设置排序 */
	if (fc_use_sort)
		fc_tuplesort = tuplesort_begin_cluster(fc_oldTupDesc, fc_OldIndex,
											maintenance_work_mem,
											NULL, TUPLESORT_NONE);
	else
		fc_tuplesort = NULL;

	/*
	 * 准备扫描 OldHeap。为了确保我们看到仍需要复制的最近死亡的元组，我们使用 SnapshotAny 扫描，并使用 HeapTupleSatisfiesVacuum 进行可见性测试。
	 */
	if (fc_OldIndex != NULL && !fc_use_sort)
	{
		const int	fc_ci_index[] = {
			PROGRESS_CLUSTER_PHASE,
			PROGRESS_CLUSTER_INDEX_RELID
		};
		int64		fc_ci_val[2];

		/* 设置阶段和 OIDOldIndex 到列 */
		fc_ci_val[0] = PROGRESS_CLUSTER_PHASE_INDEX_SCAN_HEAP;
		fc_ci_val[1] = RelationGetRelid(fc_OldIndex);
		pgstat_progress_update_multi_param(2, fc_ci_index, fc_ci_val);

		fc_tableScan = NULL;
		fc_heapScan = NULL;
		fc_indexScan = index_beginscan(fc_OldHeap, fc_OldIndex, SnapshotAny, 0, 0);
		index_rescan(fc_indexScan, NULL, 0, NULL, 0);
	}
	else
	{
		/* 在扫描和排序模式以及 VACUUM FULL 时，设置阶段 */
		pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
									 PROGRESS_CLUSTER_PHASE_SEQ_SCAN_HEAP);

		fc_tableScan = table_beginscan(fc_OldHeap, SnapshotAny, 0, (ScanKey) NULL);
		fc_heapScan = (HeapScanDesc) fc_tableScan;
		fc_indexScan = NULL;

		/* 设置总堆块 */
		pgstat_progress_update_param(PROGRESS_CLUSTER_TOTAL_HEAP_BLKS,
									 fc_heapScan->rs_nblocks);
	}

	fc_slot = table_slot_create(fc_OldHeap, NULL);
	fc_hslot = (BufferHeapTupleTableSlot *) fc_slot;

	/*
	 * 通过 OldHeap 扫描，无论是按照 OldIndex 顺序还是顺序；将每个元组复制到 NewHeap，或者暂时复制到 tuplesort 模块。请注意，我们不去排序死亡的元组（它们无论如何也不会进入新表）。
	 */
	for (;;)
	{
		HeapTuple	fc_tuple;
		Buffer		fc_buf;
		bool		fc_isdead;

		CHECK_FOR_INTERRUPTS();

		if (fc_indexScan != NULL)
		{
			if (!index_getnext_slot(fc_indexScan, ForwardScanDirection, fc_slot))
				break;

			/* 由于我们没有使用扫描键，因此不需要重新检查 */
			if (fc_indexScan->xs_recheck)
				elog(ERROR, "CLUSTER does not support lossy index conditions");
		}
		else
		{
			if (!table_scan_getnextslot(fc_tableScan, ForwardScanDirection, fc_slot))
			{
				/*
				 * 如果扫描的最后几页是空的，我们会在 heap_blks_scanned != heap_blks_total 的情况下进入下一个阶段。
				 * 相反，为了确保 heap_blks_scanned 在表扫描阶段结束后等于 total_heap_blks，
				 * 当表扫描完成时，会手动将此参数更新为正确的值。
				 */
				pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED,
											 fc_heapScan->rs_nblocks);
				break;
			}

			/*
			 * 在扫描和排序模式以及 VACUUM FULL 中，设置已扫描的堆块
			 *
			 * 注意，heapScan 可能从偏移量开始并环绕，即 rs_startblock 可能 >0，
			 * 且 rs_cblock 可能以低于 rs_startblock 的数字结束。为了防止将
			 * 此环绕情况显示给用户，我们通过 rs_startblock（模 rs_nblocks）偏移 rs_cblock。
			 */
			if (fc_prev_cblock != fc_heapScan->rs_cblock)
			{
				pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_BLKS_SCANNED,
											 (fc_heapScan->rs_cblock +
											  fc_heapScan->rs_nblocks -
											  fc_heapScan->rs_startblock
											  ) % fc_heapScan->rs_nblocks + 1);
				fc_prev_cblock = fc_heapScan->rs_cblock;
			}
		}

		fc_tuple = ExecFetchSlotHeapTuple(fc_slot, false, NULL);
		fc_buf = fc_hslot->buffer;

		LockBuffer(fc_buf, BUFFER_LOCK_SHARE);

		switch (HeapTupleSatisfiesVacuum(fc_tuple, fc_OldestXmin, fc_buf))
		{
			case HEAPTUPLE_DEAD:
				/* 明确死亡 */
				fc_isdead = true;
				break;
			case HEAPTUPLE_RECENTLY_DEAD:
				*fc_tups_recently_dead += 1;
				/* fall through */
			case HEAPTUPLE_LIVE:
				/* 活着或最近死亡，必须复制它 */
				fc_isdead = false;
				break;
			case HEAPTUPLE_INSERT_IN_PROGRESS:

				/*
				 * 由于我们对关系持有独占锁，通常看到这个的唯一方式是它在我们
				 * 自己的事务中之前被插入。然而，在系统目录中可能会发生这种情况，
				 * 因为我们往往在提交之前释放写锁。 如果没有适用的情况，则发出警告；
				 * 不过，在任何情况下我们最好复制它。
				 */
				if (!fc_is_system_catalog &&
					!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(fc_tuple->t_data)))
					elog(WARNING, "concurrent insert in progress within table \"%s\"",
						 RelationGetRelationName(fc_OldHeap));
				/* 视为活着 */
				fc_isdead = false;
				break;
			case HEAPTUPLE_DELETE_IN_PROGRESS:

				/*
				 * 类似于 INSERT_IN_PROGRESS 情况。
				 */
				if (!fc_is_system_catalog &&
					!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(fc_tuple->t_data)))
					elog(WARNING, "concurrent delete in progress within table \"%s\"",
						 RelationGetRelationName(fc_OldHeap));
				/* 视为最近死亡 */
				*fc_tups_recently_dead += 1;
				fc_isdead = false;
				break;
			default:
				elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result");
				fc_isdead = false; /* 保持编译器安静 */
				break;
		}

		LockBuffer(fc_buf, BUFFER_LOCK_UNLOCK);

		if (fc_isdead)
		{
			*fc_tups_vacuumed += 1;
			/* 堆重写模块仍需看到它... */
			if (rewrite_heap_dead_tuple(fc_rwstate, fc_tuple))
			{
				/* 之前的最近死亡元组现在被认为是死亡的 */
				*fc_tups_vacuumed += 1;
				*fc_tups_recently_dead -= 1;
			}
			continue;
		}

		*fc_num_tuples += 1;
		if (fc_tuplesort != NULL)
		{
			tuplesort_putheaptuple(fc_tuplesort, fc_tuple);

			/*
			 * 在扫描和排序模式下，报告扫描的元组数量增加
			 */
			pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED,
										 *fc_num_tuples);
		}
		else
		{
			const int	fc_ct_index[] = {
				PROGRESS_CLUSTER_HEAP_TUPLES_SCANNED,
				PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN
			};
			int64		fc_ct_val[2];

			fc_reform_and_rewrite_tuple(fc_tuple, fc_OldHeap, fc_NewHeap,
									 fc_values, fc_isnull, fc_rwstate);

			/*
			 * 在索引扫描模式及 VACUUM FULL 中，报告扫描和写入的
			 * 元组数量增加
			 */
			fc_ct_val[0] = *fc_num_tuples;
			fc_ct_val[1] = *fc_num_tuples;
			pgstat_progress_update_multi_param(2, fc_ct_index, fc_ct_val);
		}
	}

	if (fc_indexScan != NULL)
		index_endscan(fc_indexScan);
	if (fc_tableScan != NULL)
		table_endscan(fc_tableScan);
	if (fc_slot)
		ExecDropSingleTupleTableSlot(fc_slot);

	/*
	 * 在扫描和排序模式下，完成排序，然后从元组存储中读取所有活跃元组
	 * 并将它们写入新的关系中。
	 */
	if (fc_tuplesort != NULL)
	{
		double		fc_n_tuples = 0;

		/* 报告我们现在正在排序元组 */
		pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
									 PROGRESS_CLUSTER_PHASE_SORT_TUPLES);

		tuplesort_performsort(fc_tuplesort);

		/* 报告我们现在正在写入新堆 */
		pgstat_progress_update_param(PROGRESS_CLUSTER_PHASE,
									 PROGRESS_CLUSTER_PHASE_WRITE_NEW_HEAP);

		for (;;)
		{
			HeapTuple	fc_tuple;

			CHECK_FOR_INTERRUPTS();

			fc_tuple = tuplesort_getheaptuple(fc_tuplesort, true);
			if (fc_tuple == NULL)
				break;

			fc_n_tuples += 1;
			fc_reform_and_rewrite_tuple(fc_tuple,
									 fc_OldHeap, fc_NewHeap,
									 fc_values, fc_isnull,
									 fc_rwstate);
			/* 报告 n_tuples */
			pgstat_progress_update_param(PROGRESS_CLUSTER_HEAP_TUPLES_WRITTEN,
										 fc_n_tuples);
		}

		tuplesort_end(fc_tuplesort);
	}

	/* 写出任何剩余的元组，并在需要时 fsync */
	end_heap_rewrite(fc_rwstate);

	/* 清理 */
	pfree(fc_values);
	pfree(fc_isnull);
}

static bool fc_heapam_scan_analyze_next_block(TableScanDesc fc_scan, BlockNumber fc_blockno,
							   BufferAccessStrategy fc_bstrategy)
{
	HeapScanDesc fc_hscan = (HeapScanDesc) fc_scan;

	/*
	 * 我们必须对目标页面的缓冲区保持锁定，以确保并发活动——例如
	 * HOT 修剪——不会在我们查看时删除元组。因此，在查看完之前，锁定页面。
	 * 我们还决定在整个过程中持有共享锁——我们可以为每个元组释放和重新获取共享锁，
	 * 但由于我们对每个元组的工作量不大，因此额外的锁流量可能更好避免。
	 */
	fc_hscan->rs_cblock = fc_blockno;
	fc_hscan->rs_cindex = FirstOffsetNumber;
	fc_hscan->rs_cbuf = ReadBufferExtended(fc_scan->rs_rd, MAIN_FORKNUM,
										fc_blockno, RBM_NORMAL, fc_bstrategy);
	LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_SHARE);

	/* 在堆中，所有块都可以包含元组，因此始终返回 true */
	return true;
}

static bool fc_heapam_scan_analyze_next_tuple(TableScanDesc fc_scan, TransactionId fc_OldestXmin,
							   double *fc_liverows, double *fc_deadrows,
							   TupleTableSlot *fc_slot)
{
	HeapScanDesc fc_hscan = (HeapScanDesc) fc_scan;
	Page		fc_targpage;
	OffsetNumber fc_maxoffset;
	BufferHeapTupleTableSlot *fc_hslot;

	Assert(TTS_IS_BUFFERTUPLE(fc_slot));

	fc_hslot = (BufferHeapTupleTableSlot *) fc_slot;
	fc_targpage = BufferGetPage(fc_hscan->rs_cbuf);
	fc_maxoffset = PageGetMaxOffsetNumber(fc_targpage);

	/* 内部循环遍历选定页面上的所有元组 */
	for (; fc_hscan->rs_cindex <= fc_maxoffset; fc_hscan->rs_cindex++)
	{
		ItemId		fc_itemid;
		HeapTuple	fc_targtuple = &fc_hslot->base.tupdata;
		bool		fc_sample_it = false;

		fc_itemid = PageGetItemId(fc_targpage, fc_hscan->rs_cindex);

		/*
		 * 我们忽略未使用的和重定向的行指针。死亡的行指针
		 * 应被视为死亡，因为我们需要运行 VACUUM 来清除它们。
		 * 请注意，这条规则与 heap_page_prune() 计算的方式是一致的。
		 */
		if (!ItemIdIsNormal(fc_itemid))
		{
			if (ItemIdIsDead(fc_itemid))
				*fc_deadrows += 1;
			continue;
		}

		ItemPointerSet(&fc_targtuple->t_self, fc_hscan->rs_cblock, fc_hscan->rs_cindex);

		fc_targtuple->t_tableOid = RelationGetRelid(fc_scan->rs_rd);
		fc_targtuple->t_data = (HeapTupleHeader) PageGetItem(fc_targpage, fc_itemid);
		fc_targtuple->t_len = ItemIdGetLength(fc_itemid);

		switch (HeapTupleSatisfiesVacuum(fc_targtuple, fc_OldestXmin,
										 fc_hscan->rs_cbuf))
		{
			case HEAPTUPLE_LIVE:
				fc_sample_it = true;
				*fc_liverows += 1;
				break;

			case HEAPTUPLE_DEAD:
			case HEAPTUPLE_RECENTLY_DEAD:
				/* 计算死亡和最近死亡的行 */
				*fc_deadrows += 1;
				break;

			case HEAPTUPLE_INSERT_IN_PROGRESS:

				/*
				 * 正在插入的行不被计入。我们假设当插入事务提交或中止时，它将发送统计消息以增加正确的计数。只有在该事务在我们完成分析表之后结束时，这样的工作才是正确的；如果事情发生在另一种顺序，它的统计更新将被我们的覆盖。然而，只有当其他事务运行足够长的时间以插入许多元组时，错误才会很大，因此假设它会在我们之后完成是更安全的选项。
				 *
				 * 一个特殊情况是插入事务可能是我们自己的。在这种情况下，我们应该对该行进行计数和采样，以适应那些在一个事务中加载表并分析它的用户。 （pgstat_report_analyze必须调整我们向累积统计系统报告的数字以使其正确。）
				 */
				if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(fc_targtuple->t_data)))
				{
					fc_sample_it = true;
					*fc_liverows += 1;
				}
				break;

			case HEAPTUPLE_DELETE_IN_PROGRESS:

				/*
				 * 我们对正在删除的行的计数和采样与正在存活的行相同，因此如果删除的事务在我们之后提交，统计计数器就会正确，原因与上述相同。
				 *
				 * 然而，如果删除是由我们自己的事务执行的，我们必须将该行计为死行，以使pgstat_report_analyze的统计调整结果正确。 （注意：当该行在我们的事务中被插入和删除时，这一点是正确的。）
				 *
				 * 这些选择的净效果是，除了假设我们的事务已经发生外，我们的行为就像一个正在进行的事务尚未发生。
				 *
				 * 这种方法确保了如果我们看到被并发事务更新的行的前图像和后图像行时，我们将做出理智的行为：我们将对前图像进行采样，但不对后图像进行采样。如果并发事务从未提交，我们也会得到合理的结果。
				 */
				if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(fc_targtuple->t_data)))
					*fc_deadrows += 1;
				else
				{
					fc_sample_it = true;
					*fc_liverows += 1;
				}
				break;

			default:
				elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result");
				break;
		}

		if (fc_sample_it)
		{
			ExecStoreBufferHeapTuple(fc_targtuple, fc_slot, fc_hscan->rs_cbuf);
			fc_hscan->rs_cindex++;

			/* 注意我们在这里锁定了缓冲区！ */
			return true;
		}
	}

	/* 现在释放页面上的锁和钉 */
	UnlockReleaseBuffer(fc_hscan->rs_cbuf);
	fc_hscan->rs_cbuf = InvalidBuffer;

	/* 还防止旧槽的内容在页面上有钉 */
	ExecClearTuple(fc_slot);

	return false;
}

static double fc_heapam_index_build_range_scan(Relation fc_heapRelation,
							  Relation fc_indexRelation,
							  IndexInfo *fc_indexInfo,
							  bool fc_allow_sync,
							  bool fc_anyvisible,
							  bool fc_progress,
							  BlockNumber fc_start_blockno,
							  BlockNumber fc_numblocks,
							  IndexBuildCallback fc_callback,
							  void *fc_callback_state,
							  TableScanDesc fc_scan)
{
	HeapScanDesc fc_hscan;
	bool		fc_is_system_catalog;
	bool		fc_checking_uniqueness;
	HeapTuple	fc_heapTuple;
	Datum		fc_values[INDEX_MAX_KEYS];
	bool		fc_isnull[INDEX_MAX_KEYS];
	double		fc_reltuples;
	ExprState  *fc_predicate;
	TupleTableSlot *fc_slot;
	EState	   *fc_estate;
	ExprContext *fc_econtext;
	Snapshot	fc_snapshot;
	bool		fc_need_unregister_snapshot = false;
	TransactionId fc_OldestXmin;
	BlockNumber fc_previous_blkno = InvalidBlockNumber;
	BlockNumber fc_root_blkno = InvalidBlockNumber;
	OffsetNumber fc_root_offsets[MaxHeapTuplesPerPage];

	/*
	 * 健康检查
	 */
	Assert(OidIsValid(fc_indexRelation->rd_rel->relam));

	/* 记住它是否是系统目录 */
	fc_is_system_catalog = IsSystemRelation(fc_heapRelation);

	/* 查看我们是否在验证唯一性/排除属性 */
	fc_checking_uniqueness = (fc_indexInfo->ii_Unique ||
						   fc_indexInfo->ii_ExclusionOps != NULL);

	/*
	 * "任何可见"模式与唯一性检查不兼容；确保只请求其中一个。
	 */
	Assert(!(fc_anyvisible && fc_checking_uniqueness));

	/*
	 * 需要一个EState来评估索引表达式和部分索引谓词。 还需要一个槽来保存当前元组。
	 */
	fc_estate = CreateExecutorState();
	fc_econtext = GetPerTupleExprContext(fc_estate);
	fc_slot = table_slot_create(fc_heapRelation, NULL);

	/* 安排econtext的扫描元组为待测试的元组 */
	fc_econtext->ecxt_scantuple = fc_slot;

	/* 如果有的话，为谓词设置执行状态。 */
	fc_predicate = ExecPrepareQual(fc_indexInfo->ii_Predicate, fc_estate);

	/*
	 * 准备扫描基础关系。在正常的索引构建中，我们使用
	 * SnapshotAny，因为我们必须检索所有元组并进行自己的时间
	 * 资格检查（因为我们必须索引最近死亡的元组）。在一个
	 * 并发构建中，或者在引导期间，我们采取一个常规的MVCC快照
	 * 并根据它索引所有活的元组。
	 */
	fc_OldestXmin = InvalidTransactionId;

	/* 可以在此处忽略惰性VACUUM */
	if (!IsBootstrapProcessingMode() && !fc_indexInfo->ii_Concurrent)
		fc_OldestXmin = GetOldestNonRemovableTransactionId(fc_heapRelation);

	if (!fc_scan)
	{
		/*
		 * 串行索引构建。
		 *
		 * 在这种情况下必须开始自己的堆扫描。我们可能还需要
		 * 注册一个在我们直接控制之下的快照。
		 */
		if (!TransactionIdIsValid(fc_OldestXmin))
		{
			fc_snapshot = RegisterSnapshot(GetTransactionSnapshot());
			fc_need_unregister_snapshot = true;
		}
		else
			fc_snapshot = SnapshotAny;

		fc_scan = table_beginscan_strat(fc_heapRelation,	/* 关系 */
									 fc_snapshot,	/* 快照 */
									 0, /* 键的数量 */
									 NULL,	/* 扫描键 */
									 true,	/* 缓冲区访问策略OK */
									 fc_allow_sync);	/* 允许同步扫描吗？ */
	}
	else
	{
		/*
		 * 并行索引构建。
		 *
		 * 并行情况下永远不会注册/注销自己的快照。快照
		 * 是从并行堆扫描中获取的，是SnapshotAny或一个MVCC
		 * 快照，基于与串行情况相同的标准。
		 */
		Assert(!IsBootstrapProcessingMode());
		Assert(fc_allow_sync);
		fc_snapshot = fc_scan->rs_snapshot;
	}

	fc_hscan = (HeapScanDesc) fc_scan;

	/*
	 * 如果使用
	 * SnapshotAny，必须调用GetOldestNonRemovableTransactionId()。对于MVCC快照则不应该。
	 * （特别值得检查并行构建的情况，因为支持并行构建的
	 * ambuild例程必须自行处理这些细节。）
	 */
	Assert(fc_snapshot == SnapshotAny || IsMVCCSnapshot(fc_snapshot));
	Assert(fc_snapshot == SnapshotAny ? TransactionIdIsValid(fc_OldestXmin) :
		   !TransactionIdIsValid(fc_OldestXmin));
	Assert(fc_snapshot == SnapshotAny || !fc_anyvisible);

	/* 发布要扫描的块数 */
	if (fc_progress)
	{
		BlockNumber fc_nblocks;

		if (fc_hscan->rs_base.rs_parallel != NULL)
		{
			ParallelBlockTableScanDesc fc_pbscan;

			fc_pbscan = (ParallelBlockTableScanDesc) fc_hscan->rs_base.rs_parallel;
			fc_nblocks = fc_pbscan->phs_nblocks;
		}
		else
			fc_nblocks = fc_hscan->rs_nblocks;

		pgstat_progress_update_param(PROGRESS_SCAN_BLOCKS_TOTAL,
									 fc_nblocks);
	}

	/* 设置我们的扫描端点 */
	if (!fc_allow_sync)
		heap_setscanlimits(fc_scan, fc_start_blockno, fc_numblocks);
	else
	{
		/* 同步扫描只能请求整个关系 */
		Assert(fc_start_blockno == 0);
		Assert(fc_numblocks == InvalidBlockNumber);
	}

	fc_reltuples = 0;

	/*
	 * 扫描基础关系中的所有元组。
	 */
	while ((fc_heapTuple = heap_getnext(fc_scan, ForwardScanDirection)) != NULL)
	{
		bool		fc_tupleIsAlive;

		CHECK_FOR_INTERRUPTS();

		/* 如果被要求，报告扫描进度。 */
		if (fc_progress)
		{
			BlockNumber fc_blocks_done = fc_heapam_scan_get_blocks_done(fc_hscan);

			if (fc_blocks_done != fc_previous_blkno)
			{
				pgstat_progress_update_param(PROGRESS_SCAN_BLOCKS_DONE,
											 fc_blocks_done);
				fc_previous_blkno = fc_blocks_done;
			}
		}

		/*
		 * 在处理一系列更新的HOT链时，我们希望索引活跃元组的值（如果有的话），但要在链的根元组的TID下进行索引。 这种方法是必要的，以保持堆中的HOT链结构。因此，我们需要能够找到每个在HOT链中的元组的根项偏移。当首次到达关系的新页面时，调用heap_get_root_tuples()来构建页面上根项偏移的映射。
		 *
		 * 在缓冲区锁定/解锁之间使用这些信息可能看起来不安全。 然而，我们在表上保持ShareLock，因此不应发生普通的插入/更新/删除；而且我们在访问页面时持续保持缓冲区的pin，因此也不可能发生修剪操作。
		 *
		 * 在仅对表具有ShareUpdateExclusiveLock的情况下，可能会出现一些我们在首次读取页面时不知道的HOT元组。 为了处理这种情况，当HOT元组指向我们不知道的根项时，我们会重新获取根偏移列表。
		 *
		 * 此外，尽管在扫描页面时我们对元组生存状态的看法可能会改变（由于并发事务提交/中止），但链根位置不会改变，因此在等待另一个事务后不需要重新构建这些信息。
		 *
		 * 请注意隐含的假设：每个HOT链中不会有超过一个活跃元组——否则我们可能会创建指向同一根元组的多个索引条目。
		 */
		if (fc_hscan->rs_cblock != fc_root_blkno)
		{
			Page		fc_page = BufferGetPage(fc_hscan->rs_cbuf);

			LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_SHARE);
			heap_get_root_tuples(fc_page, fc_root_offsets);
			LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_UNLOCK);

			fc_root_blkno = fc_hscan->rs_cblock;
		}

		if (fc_snapshot == SnapshotAny)
		{
			/* 自己进行时间资格检查 */
			bool		fc_indexIt;
			TransactionId fc_xwait;

	recheck:

			/*
			 * 我们可能可以不在这里锁定缓冲区，因为调用者应该在关系上保持ShareLock，但我们还是谨慎一点。 （即使在HOT修剪情况下，这条评论仍然正确：我们对缓冲区的pin防止了修剪。）
			 */
			LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_SHARE);

			/*
			 * 将元组计为活跃的标准需要与analyze.c的heapam_scan_analyze_next_tuple()所做的匹配，否则CREATE INDEX和ANALYZE可能会产生截然不同的reltuples值，例如，当有许多最近死亡的元组时。
			 */
			switch (HeapTupleSatisfiesVacuum(fc_heapTuple, fc_OldestXmin,
											 fc_hscan->rs_cbuf))
			{
				case HEAPTUPLE_DEAD:
					/* 绝对死亡，我们可以忽略它 */
					fc_indexIt = false;
					fc_tupleIsAlive = false;
					break;
				case HEAPTUPLE_LIVE:
					/* 正常情况，索引并进行唯一性检查 */
					fc_indexIt = true;
					fc_tupleIsAlive = true;
					/* 也将其计为活跃 */
					fc_reltuples += 1;
					break;
				case HEAPTUPLE_RECENTLY_DEAD:

					/*
					 * 如果元组是最近删除的，那么我们必须对其进行索引，以保持MVCC语义。 （预存在的事务可能会在我们完成构建索引后尝试使用该索引，并可能需要查看这些元组。）
					 *
					 * 然而，如果它是HOT更新的，那么我们必须只在HOT链的末尾对活跃元组进行索引。 由于这打破了对预存在快照的语义，标记该索引为对它们不可用。
					 *
					 * 即使我们对最近死亡的元组进行了索引，也不在reltuples中计入它们；参见heapam_scan_analyze_next_tuple()。
					 */
					if (HeapTupleIsHotUpdated(fc_heapTuple))
					{
						fc_indexIt = false;
						/* 将索引标记为对旧快照不安全 */
						fc_indexInfo->ii_BrokenHotChain = true;
					}
					else
						fc_indexIt = true;
					/* 在任何情况下，将元组排除在唯一性检查之外 */
					fc_tupleIsAlive = false;
					break;
				case HEAPTUPLE_INSERT_IN_PROGRESS:

					/*
					 * 在“任何可见”模式下，该元组是可见的，我们
					 * 无需进一步检查。
					 */
					if (fc_anyvisible)
					{
						fc_indexIt = true;
						fc_tupleIsAlive = true;
						fc_reltuples += 1;
						break;
					}

					/*
					 * 由于调用者应该持有 ShareLock 或更高级别的锁，
					 * 通常情况下，看到这个元组的唯一方法是它在我们自己的
					 * 事务中是较早插入的。然而，在系统目录中，也可能发生
					 * 这种情况，因为我们倾向于在提交之前释放写锁。
					 * 如果两种情况都不适用，则发出警告。
					 */
					fc_xwait = HeapTupleHeaderGetXmin(fc_heapTuple->t_data);
					if (!TransactionIdIsCurrentTransactionId(fc_xwait))
					{
						if (!fc_is_system_catalog)
							elog(WARNING, "concurrent insert in progress within table \"%s\"",
								 RelationGetRelationName(fc_heapRelation));

						/*
						 * 如果我们正在进行唯一性检查，索引
						 * 这样的元组可能导致虚假的唯一性
						 * 失败。在这种情况下，我们等待插入
						 * 事务完成后再检查。
						 */
						if (fc_checking_uniqueness)
						{
							/*
							 * 在等待之前必须释放对缓冲区的锁
							 */
							LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_UNLOCK);
							XactLockTableWait(fc_xwait, fc_heapRelation,
											  &fc_heapTuple->t_self,
											  XLTW_InsertIndexUnique);
							CHECK_FOR_INTERRUPTS();
							goto recheck;
						}
					}
					else
					{
						/*
						 * 为了与
						 * heapam_scan_analyze_next_tuple() 一致，
						 * 只有在我们的事务插入时，才将
						 * HEAPTUPLE_INSERT_IN_PROGRESS 元组计为活动。
						 */
						fc_reltuples += 1;
					}

					/*
					 * 我们必须索引这样的元组，因为如果索引构建
					 * 提交，那么它们就是有效的。
					 */
					fc_indexIt = true;
					fc_tupleIsAlive = true;
					break;
				case HEAPTUPLE_DELETE_IN_PROGRESS:

					/*
					 * 与 INSERT_IN_PROGRESS 情况一样，除非这是我们自己的
					 * 删除或系统目录，否则这是意外的；但是在任何可见模式下，
					 * 该元组是可见的。
					 */
					if (fc_anyvisible)
					{
						fc_indexIt = true;
						fc_tupleIsAlive = false;
						fc_reltuples += 1;
						break;
					}

					fc_xwait = HeapTupleHeaderGetUpdateXid(fc_heapTuple->t_data);
					if (!TransactionIdIsCurrentTransactionId(fc_xwait))
					{
						if (!fc_is_system_catalog)
							elog(WARNING, "concurrent delete in progress within table \"%s\"",
								 RelationGetRelationName(fc_heapRelation));

						/*
						 * 如果我们正在进行唯一性检查，假设
						 * 该元组已死亡可能会导致错过一个
						 * 唯一性违反。在这种情况下，我们等待
						 * 删除事务完成后再检查。
						 *
						 * 此外，如果这是一个 HOT 更新的元组，我们不应该
						 * 对其进行索引，而是对 HOT 链末尾的活动元组进行索引。
						 * 然而，删除事务可能会中止，从而可能使该元组最终仍然
						 * 活动，在这种情况下必须对其进行索引。知道该做什么的
						 * 唯一方法是等待删除事务完成后再检查。
						 */
						if (fc_checking_uniqueness ||
							HeapTupleIsHotUpdated(fc_heapTuple))
						{
							/*
							 * 在等待之前必须释放对缓冲区的锁
							 */
							LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_UNLOCK);
							XactLockTableWait(fc_xwait, fc_heapRelation,
											  &fc_heapTuple->t_self,
											  XLTW_InsertIndexUnique);
							CHECK_FOR_INTERRUPTS();
							goto recheck;
						}

						/*
						 * 否则索引它，但不检查唯一性，
						 * 与最近死亡的元组相同。
						 */
						fc_indexIt = true;

						/*
						 * 将 HEAPTUPLE_DELETE_IN_PROGRESS 元组计为活动，
						 * 如果它们不是由当前事务删除的。
						 * 这就是 heapam_scan_analyze_next_tuple() 所做的，
						 * 我们希望行为保持一致。
						 */
						fc_reltuples += 1;
					}
					else if (HeapTupleIsHotUpdated(fc_heapTuple))
					{
						/*
						 * 这是一个由我们自己的事务删除的 HOT 更新的元组。
						 * 我们可以假设删除将提交（否则索引内容无关紧要），
						 * 因此将其视为最近死亡的 HOT 更新元组。
						 */
						fc_indexIt = false;
						/* 将索引标记为对旧快照不安全 */
						fc_indexInfo->ii_BrokenHotChain = true;
					}
					else
					{
						/*
						 * 这是一个由我们自己的事务删除的常规元组。
						 * 对其进行索引，但不检查唯一性，也不计入
						 * reltuples，与最近死亡的元组相同。
						 */
						fc_indexIt = true;
					}
					/* 在任何情况下，将元组排除在唯一性检查之外 */
					fc_tupleIsAlive = false;
					break;
				default:
					elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result");
					fc_indexIt = fc_tupleIsAlive = false; /* 保持编译器安静 */
					break;
			}

			LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_UNLOCK);

			if (!fc_indexIt)
				continue;
		}
		else
		{
			/* heap_getnext进行了时间条件检查 */
			fc_tupleIsAlive = true;
			fc_reltuples += 1;
		}

		MemoryContextReset(fc_econtext->ecxt_per_tuple_memory);

		/* 为谓词或表达式评估做好准备 */
		ExecStoreBufferHeapTuple(fc_heapTuple, fc_slot, fc_hscan->rs_cbuf);

		/*
		 * 在部分索引中，丢弃不满足谓词的元组。
		 */
		if (fc_predicate != NULL)
		{
			if (!ExecQual(fc_predicate, fc_econtext))
				continue;
		}

		/*
		 * 对于当前的堆元组，提取我们在此索引中使用的所有属性，并注意哪些是空值。这还会执行所需表达式的评估。
		 */
		FormIndexDatum(fc_indexInfo,
					   fc_slot,
					   fc_estate,
					   fc_values,
					   fc_isnull);

		/*
		 * 你可能会认为我们应该在这里继续构建索引元组，但某些索引访问方法想先对数据进行进一步处理。所以传递值数组[]和空值数组[]。
		 */

		if (HeapTupleIsHeapOnly(fc_heapTuple))
		{
			/*
			 * 对于仅堆元组，假装它的TID是根的TID。有关讨论，请参见src/backend/access/heap/README.HOT。
			 */
			ItemPointerData fc_tid;
			OffsetNumber fc_offnum;

			fc_offnum = ItemPointerGetOffsetNumber(&fc_heapTuple->t_self);

			/*
			 * 如果一个HOT元组指向一个我们不知道的根，重新获取根项。如果仍然失败，将其报告为损坏。
			 */
			if (fc_root_offsets[fc_offnum - 1] == InvalidOffsetNumber)
			{
				Page		fc_page = BufferGetPage(fc_hscan->rs_cbuf);

				LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_SHARE);
				heap_get_root_tuples(fc_page, fc_root_offsets);
				LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_UNLOCK);
			}

			if (!OffsetNumberIsValid(fc_root_offsets[fc_offnum - 1]))
				ereport(ERROR,
						(errcode(ERRCODE_DATA_CORRUPTED),
						 errmsg_internal("failed to find parent tuple for heap-only tuple at (%u,%u) in table \"%s\"",
										 ItemPointerGetBlockNumber(&fc_heapTuple->t_self),
										 fc_offnum,
										 RelationGetRelationName(fc_heapRelation))));

			ItemPointerSet(&fc_tid, ItemPointerGetBlockNumber(&fc_heapTuple->t_self),
						   fc_root_offsets[fc_offnum - 1]);

			/* 调用AM的回调例程来处理元组 */
			fc_callback(fc_indexRelation, &fc_tid, fc_values, fc_isnull, fc_tupleIsAlive,
					 fc_callback_state);
		}
		else
		{
			/* 调用AM的回调例程来处理元组 */
			fc_callback(fc_indexRelation, &fc_heapTuple->t_self, fc_values, fc_isnull,
					 fc_tupleIsAlive, fc_callback_state);
		}
	}

	/* 最后一次报告扫描进度。 */
	if (fc_progress)
	{
		BlockNumber fc_blks_done;

		if (fc_hscan->rs_base.rs_parallel != NULL)
		{
			ParallelBlockTableScanDesc fc_pbscan;

			fc_pbscan = (ParallelBlockTableScanDesc) fc_hscan->rs_base.rs_parallel;
			fc_blks_done = fc_pbscan->phs_nblocks;
		}
		else
			fc_blks_done = fc_hscan->rs_nblocks;

		pgstat_progress_update_param(PROGRESS_SCAN_BLOCKS_DONE,
									 fc_blks_done);
	}

	table_endscan(fc_scan);

	/* 如果由我们设置和注册，现在可以忘记我们的快照 */
	if (fc_need_unregister_snapshot)
		UnregisterSnapshot(fc_snapshot);

	ExecDropSingleTupleTableSlot(fc_slot);

	FreeExecutorState(fc_estate);

	/* 这些可能指向现在已经消失的状态 */
	fc_indexInfo->ii_ExpressionsState = NIL;
	fc_indexInfo->ii_PredicateState = NULL;

	return fc_reltuples;
}

static void fc_heapam_index_validate_scan(Relation fc_heapRelation,
						   Relation fc_indexRelation,
						   IndexInfo *fc_indexInfo,
						   Snapshot fc_snapshot,
						   ValidateIndexState *fc_state)
{
	TableScanDesc fc_scan;
	HeapScanDesc fc_hscan;
	HeapTuple	fc_heapTuple;
	Datum		fc_values[INDEX_MAX_KEYS];
	bool		fc_isnull[INDEX_MAX_KEYS];
	ExprState  *fc_predicate;
	TupleTableSlot *fc_slot;
	EState	   *fc_estate;
	ExprContext *fc_econtext;
	BlockNumber fc_root_blkno = InvalidBlockNumber;
	OffsetNumber fc_root_offsets[MaxHeapTuplesPerPage];
	bool		fc_in_index[MaxHeapTuplesPerPage];
	BlockNumber fc_previous_blkno = InvalidBlockNumber;

	/* 合并的状态变量 */
	ItemPointer fc_indexcursor = NULL;
	ItemPointerData fc_decoded;
	bool		fc_tuplesort_empty = false;

	/*
	 * 健康检查
	 */
	Assert(OidIsValid(fc_indexRelation->rd_rel->relam));

	/*
	 * 需要一个EState来评估索引表达式和部分索引谓词。 还需要一个槽来保存当前元组。
	 */
	fc_estate = CreateExecutorState();
	fc_econtext = GetPerTupleExprContext(fc_estate);
	fc_slot = MakeSingleTupleTableSlot(RelationGetDescr(fc_heapRelation),
									&TTSOpsHeapTuple);

	/* 安排econtext的扫描元组为待测试的元组 */
	fc_econtext->ecxt_scantuple = fc_slot;

	/* 如果有的话，为谓词设置执行状态。 */
	fc_predicate = ExecPrepareQual(fc_indexInfo->ii_Predicate, fc_estate);

	/*
	 * 准备扫描基本关系。我们只需要那些满足传入引用快照的元组。我们必须在此禁用同步扫描，因为从块零开始读取到匹配排序的TID是至关重要的。
	 */
	fc_scan = table_beginscan_strat(fc_heapRelation,	/* 关系 */
								 fc_snapshot,	/* 快照 */
								 0, /* 键的数量 */
								 NULL,	/* 扫描键 */
								 true,	/* 缓冲区访问策略OK */
								 false);	/* 不允许同步扫描 */
	fc_hscan = (HeapScanDesc) fc_scan;

	pgstat_progress_update_param(PROGRESS_SCAN_BLOCKS_TOTAL,
								 fc_hscan->rs_nblocks);

	/*
	 * 扫描所有匹配快照的元组。
	 */
	while ((fc_heapTuple = heap_getnext(fc_scan, ForwardScanDirection)) != NULL)
	{
		ItemPointer fc_heapcursor = &fc_heapTuple->t_self;
		ItemPointerData fc_rootTuple;
		OffsetNumber fc_root_offnum;

		CHECK_FOR_INTERRUPTS();

		fc_state->htups += 1;

		if ((fc_previous_blkno == InvalidBlockNumber) ||
			(fc_hscan->rs_cblock != fc_previous_blkno))
		{
			pgstat_progress_update_param(PROGRESS_SCAN_BLOCKS_DONE,
										 fc_hscan->rs_cblock);
			fc_previous_blkno = fc_hscan->rs_cblock;
		}

		/*
		 * 正如在table_index_build_scan中所评论的那样，我们应该在其根元组的TID下对仅堆元组进行索引；因此，当我们进入新堆页面时，建立页面上根项偏移量的映射。
		 *
		 * 这使得与元组排序输出的合并变得复杂：我们将按偏移量的顺序访问活动元组，但我们需要与索引内容进行比较的根偏移量可能按照不同的顺序排列。因此，我们可能不得不在元组排序输出中“回顾”，但仅限于当前页面。我们通过保持一个布尔数组in_index[]来处理该问题，该数组显示当前页面上所有已遍历的元组排序输出TID。我们在进入新的堆页面时清除该数组。
		 */
		if (fc_hscan->rs_cblock != fc_root_blkno)
		{
			Page		fc_page = BufferGetPage(fc_hscan->rs_cbuf);

			LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_SHARE);
			heap_get_root_tuples(fc_page, fc_root_offsets);
			LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_UNLOCK);

			memset(fc_in_index, 0, sizeof(fc_in_index));

			fc_root_blkno = fc_hscan->rs_cblock;
		}

		/* 将实际元组TID转换为根TID */
		fc_rootTuple = *fc_heapcursor;
		fc_root_offnum = ItemPointerGetOffsetNumber(fc_heapcursor);

		if (HeapTupleIsHeapOnly(fc_heapTuple))
		{
			fc_root_offnum = fc_root_offsets[fc_root_offnum - 1];
			if (!OffsetNumberIsValid(fc_root_offnum))
				ereport(ERROR,
						(errcode(ERRCODE_DATA_CORRUPTED),
						 errmsg_internal("failed to find parent tuple for heap-only tuple at (%u,%u) in table \"%s\"",
										 ItemPointerGetBlockNumber(fc_heapcursor),
										 ItemPointerGetOffsetNumber(fc_heapcursor),
										 RelationGetRelationName(fc_heapRelation))));
			ItemPointerSetOffsetNumber(&fc_rootTuple, fc_root_offnum);
		}

		/*
		 * 通过跳过索引元组，直到找到或超过当前根元组来“合并”。
		 */
		while (!fc_tuplesort_empty &&
			   (!fc_indexcursor ||
				ItemPointerCompare(fc_indexcursor, &fc_rootTuple) < 0))
		{
			Datum		fc_ts_val;
			bool		fc_ts_isnull;

			if (fc_indexcursor)
			{
				/*
				 * 记住当前堆页面上之前看到的索引项
				 */
				if (ItemPointerGetBlockNumber(fc_indexcursor) == fc_root_blkno)
					fc_in_index[ItemPointerGetOffsetNumber(fc_indexcursor) - 1] = true;
			}

			fc_tuplesort_empty = !tuplesort_getdatum(fc_state->tuplesort, true,
												  &fc_ts_val, &fc_ts_isnull, NULL);
			Assert(fc_tuplesort_empty || !fc_ts_isnull);
			if (!fc_tuplesort_empty)
			{
				itemptr_decode(&fc_decoded, DatumGetInt64(fc_ts_val));
				fc_indexcursor = &fc_decoded;

				/* 如果 int8 是按引用传递，释放（编码的）TID Datum 内存 */
#ifndef USE_FLOAT8_BYVAL
				pfree(DatumGetPointer(fc_ts_val));
#endif
			}
			else
			{
				/* 保持整洁 */
				fc_indexcursor = NULL;
			}
		}

		/*
		 * 如果 tuplesort 超出了 *而* 我们之前没有看到匹配，
		 * 那么这个元组在索引中缺失，因此插入它。
		 */
		if ((fc_tuplesort_empty ||
			 ItemPointerCompare(fc_indexcursor, &fc_rootTuple) > 0) &&
			!fc_in_index[fc_root_offnum - 1])
		{
			MemoryContextReset(fc_econtext->ecxt_per_tuple_memory);

			/* 为谓词或表达式评估做好准备 */
			ExecStoreHeapTuple(fc_heapTuple, fc_slot, false);

			/*
			 * 在部分索引中，丢弃不满足谓词的元组。
			 */
			if (fc_predicate != NULL)
			{
				if (!ExecQual(fc_predicate, fc_econtext))
					continue;
			}

			/*
			 * 对于当前的堆元组，提取我们在此索引中使用的所有属性，并注意哪些是空值。 
			 * 这还会执行任何需要的表达式评估。
			 */
			FormIndexDatum(fc_indexInfo,
						   fc_slot,
						   fc_estate,
						   fc_values,
						   fc_isnull);

			/*
			 * 你可能会认为我们应该在这里构建索引元组，
			 * 但某些索引 AM 想先对数据进行进一步处理。所以
			 * 传递 values[] 和 isnull[] 数组。
			 */

			/*
			 * 如果元组已经被提交为无效，你可能会认为我们
			 * 可以抑制唯一性检查，但在 HOT 存在的情况下这不再成立，
			 * 因为插入实际上是对整个 HOT 链的唯一性检查的代理。
			 * 也就是说，我们这里的元组可能是无效的，因为它已经被
			 * HOT 更新，如果是这样，更新事务将不会认为应该插入索引条目。
			 * 索引 AM 将检查整个 HOT 链，如果有冲突会正确检测出。
			 */

			index_insert(fc_indexRelation,
						 fc_values,
						 fc_isnull,
						 &fc_rootTuple,
						 fc_heapRelation,
						 fc_indexInfo->ii_Unique ?
						 UNIQUE_CHECK_YES : UNIQUE_CHECK_NO,
						 false,
						 fc_indexInfo);

			fc_state->tups_inserted += 1;
		}
	}

	table_endscan(fc_scan);

	ExecDropSingleTupleTableSlot(fc_slot);

	FreeExecutorState(fc_estate);

	/* 这些可能指向现在已经消失的状态 */
	fc_indexInfo->ii_ExpressionsState = NIL;
	fc_indexInfo->ii_PredicateState = NULL;
}

/*
 * 返回自开始以来此扫描已读取的块的数量。
 * 这用于进度报告，而不是完全准确：在并行扫描中，
 * 工作线程可能同时读取比我们报告的更前面的块。
 */
static BlockNumber fc_heapam_scan_get_blocks_done(HeapScanDesc fc_hscan)
{
	ParallelBlockTableScanDesc fc_bpscan = NULL;
	BlockNumber fc_startblock;
	BlockNumber fc_blocks_done;

	if (fc_hscan->rs_base.rs_parallel != NULL)
	{
		fc_bpscan = (ParallelBlockTableScanDesc) fc_hscan->rs_base.rs_parallel;
		fc_startblock = fc_bpscan->phs_startblock;
	}
	else
		fc_startblock = fc_hscan->rs_startblock;

	/*
	 * 如果 startblock 不为零，则可能已环绕到关系的末尾。
	 */
	if (fc_hscan->rs_cblock > fc_startblock)
		fc_blocks_done = fc_hscan->rs_cblock - fc_startblock;
	else
	{
		BlockNumber fc_nblocks;

		fc_nblocks = fc_bpscan != NULL ? fc_bpscan->phs_nblocks : fc_hscan->rs_nblocks;
		fc_blocks_done = fc_nblocks - fc_startblock +
			fc_hscan->rs_cblock;
	}

	return fc_blocks_done;
}


/* ------------------------------------------------------------------------
 * 堆 AM 的其他回调
 * ------------------------------------------------------------------------
 */

/*
 * 检查表是否需要 TOAST 表。只有在以下情况下它才需要：
 * (1) 存在可 TOAST 的属性，以及 
 * (2) 元组的最大长度可能超过 TOAST_TUPLE_THRESHOLD。
 * （我们不想为诸如 "f1 varchar(20)" 的东西创建 TOAST 表。）
 */
static bool fc_heapam_relation_needs_toast_table(Relation fc_rel)
{
	int32		fc_data_length = 0;
	bool		fc_maxlength_unknown = false;
	bool		fc_has_toastable_attrs = false;
	TupleDesc	fc_tupdesc = fc_rel->rd_att;
	int32		fc_tuple_length;
	int			fc_i;

	for (fc_i = 0; fc_i < fc_tupdesc->natts; fc_i++)
	{
		Form_pg_attribute fc_att = TupleDescAttr(fc_tupdesc, fc_i);

		if (fc_att->attisdropped)
			continue;
		fc_data_length = att_align_nominal(fc_data_length, fc_att->attalign);
		if (fc_att->attlen > 0)
		{
			/* 固定长度类型永远不可 TOAST */
			fc_data_length += fc_att->attlen;
		}
		else
		{
			int32		fc_maxlen = type_maximum_size(fc_att->atttypid,
												   fc_att->atttypmod);

			if (fc_maxlen < 0)
				fc_maxlength_unknown = true;
			else
				fc_data_length += fc_maxlen;
			if (fc_att->attstorage != TYPSTORAGE_PLAIN)
				fc_has_toastable_attrs = true;
		}
	}
	if (!fc_has_toastable_attrs)
		return false;			/* 没有什么可 TOAST 的？ */
	if (fc_maxlength_unknown)
		return true;			/* 有无限长度的属性吗？ */
	fc_tuple_length = MAXALIGN(SizeofHeapTupleHeader +
							BITMAPLEN(fc_tupdesc->natts)) +
		MAXALIGN(fc_data_length);
	return (fc_tuple_length > TOAST_TUPLE_THRESHOLD);
}

/*
 * 堆关系的 TOAST 表只是堆关系。
 */
static Oid fc_heapam_relation_toast_am(Relation fc_rel)
{
	return fc_rel->rd_rel->relam;
}


/* ------------------------------------------------------------------------
 * 与堆 AM 相关的规划器回调
 * ------------------------------------------------------------------------
 */

#define HEAP_OVERHEAD_BYTES_PER_TUPLE \
	(MAXALIGN(SizeofHeapTupleHeader) + sizeof(ItemIdData))
#define HEAP_USABLE_BYTES_PER_PAGE \
	(BLCKSZ - SizeOfPageHeaderData)

static void fc_heapam_estimate_rel_size(Relation fc_rel, int32 *fc_attr_widths,
						 BlockNumber *fc_pages, double *fc_tuples,
						 double *fc_allvisfrac)
{
	table_block_relation_estimate_size(fc_rel, fc_attr_widths, fc_pages,
									   fc_tuples, fc_allvisfrac,
									   HEAP_OVERHEAD_BYTES_PER_TUPLE,
									   HEAP_USABLE_BYTES_PER_PAGE);
}


/* ------------------------------------------------------------------------
 * 与堆 AM 相关的执行器回调
 * ------------------------------------------------------------------------
 */

static bool fc_heapam_scan_bitmap_next_block(TableScanDesc fc_scan,
							  TBMIterateResult *fc_tbmres)
{
	HeapScanDesc fc_hscan = (HeapScanDesc) fc_scan;
	BlockNumber fc_page = fc_tbmres->blockno;
	Buffer		fc_buffer;
	Snapshot	fc_snapshot;
	int			fc_ntup;

	fc_hscan->rs_cindex = 0;
	fc_hscan->rs_ntuples = 0;

	/*
	 * 忽略我们认为是关系结束后的任何声明条目。它可能在我们扫描开始后被扩展（我们只持有一个 AccessShareLock，并且可能来自这个后端的插入）。然而，在 SERIALIZABLE 隔离级别下我们不进行这种优化，因为我们需要检查所有通过索引可达的不可见元组。
	 */
	if (!IsolationIsSerializable() && fc_page >= fc_hscan->rs_nblocks)
		return false;

	/*
	 * 在目标堆页面上获取引脚，交换我们之前持有的任何引脚。
	 */
	fc_hscan->rs_cbuf = ReleaseAndReadBuffer(fc_hscan->rs_cbuf,
										  fc_scan->rs_rd,
										  fc_page);
	fc_hscan->rs_cblock = fc_page;
	fc_buffer = fc_hscan->rs_cbuf;
	fc_snapshot = fc_scan->rs_snapshot;

	fc_ntup = 0;

	/*
	 * 如果可能，修剪并修复整个页面的碎片。
	 */
	heap_page_prune_opt(fc_scan->rs_rd, fc_buffer);

	/*
	 * 在检查元组可见性时，必须对缓冲区内容持有共享锁。 然而，之后，只要我们保持缓冲区固定，我们找到的可见元组就保证是好的。
	 */
	LockBuffer(fc_buffer, BUFFER_LOCK_SHARE);

	/*
	 * 对于有损和无损情况，我们需要两种独立的策略。
	 */
	if (fc_tbmres->ntuples >= 0)
	{
		/*
		 * 位图是无损的，因此我们只需查看 tbmres 中列出的偏移量；但我们必须跟随每个此类偏移量开始的任何 HOT 链。
		 */
		int			fc_curslot;

		for (fc_curslot = 0; fc_curslot < fc_tbmres->ntuples; fc_curslot++)
		{
			OffsetNumber fc_offnum = fc_tbmres->offsets[fc_curslot];
			ItemPointerData fc_tid;
			HeapTupleData fc_heapTuple;

			ItemPointerSet(&fc_tid, fc_page, fc_offnum);
			if (heap_hot_search_buffer(&fc_tid, fc_scan->rs_rd, fc_buffer, fc_snapshot,
									   &fc_heapTuple, NULL, true))
				fc_hscan->rs_vistuples[fc_ntup++] = ItemPointerGetOffsetNumber(&fc_tid);
		}
	}
	else
	{
		/*
		 * 位图是有损的，因此我们必须检查页面上的每个行指针。但我们可以忽略 HOT 链，因为我们无论如何都会检查每个元组。
		 */
		Page		fc_dp = (Page) BufferGetPage(fc_buffer);
		OffsetNumber fc_maxoff = PageGetMaxOffsetNumber(fc_dp);
		OffsetNumber fc_offnum;

		for (fc_offnum = FirstOffsetNumber; fc_offnum <= fc_maxoff; fc_offnum = OffsetNumberNext(fc_offnum))
		{
			ItemId		fc_lp;
			HeapTupleData fc_loctup;
			bool		fc_valid;

			fc_lp = PageGetItemId(fc_dp, fc_offnum);
			if (!ItemIdIsNormal(fc_lp))
				continue;
			fc_loctup.t_data = (HeapTupleHeader) PageGetItem((Page) fc_dp, fc_lp);
			fc_loctup.t_len = ItemIdGetLength(fc_lp);
			fc_loctup.t_tableOid = fc_scan->rs_rd->rd_id;
			ItemPointerSet(&fc_loctup.t_self, fc_page, fc_offnum);
			fc_valid = HeapTupleSatisfiesVisibility(&fc_loctup, fc_snapshot, fc_buffer);
			if (fc_valid)
			{
				fc_hscan->rs_vistuples[fc_ntup++] = fc_offnum;
				PredicateLockTID(fc_scan->rs_rd, &fc_loctup.t_self, fc_snapshot,
								 HeapTupleHeaderGetXmin(fc_loctup.t_data));
			}
			HeapCheckForSerializableConflictOut(fc_valid, fc_scan->rs_rd, &fc_loctup,
												fc_buffer, fc_snapshot);
		}
	}

	LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);

	Assert(fc_ntup <= MaxHeapTuplesPerPage);
	fc_hscan->rs_ntuples = fc_ntup;

	return fc_ntup > 0;
}

static bool fc_heapam_scan_bitmap_next_tuple(TableScanDesc fc_scan,
							  TBMIterateResult *fc_tbmres,
							  TupleTableSlot *fc_slot)
{
	HeapScanDesc fc_hscan = (HeapScanDesc) fc_scan;
	OffsetNumber fc_targoffset;
	Page		fc_dp;
	ItemId		fc_lp;

	/*
	 * 超出范围？ 如果是这样，这个页面上没有更多可以查看的内容
	 */
	if (fc_hscan->rs_cindex < 0 || fc_hscan->rs_cindex >= fc_hscan->rs_ntuples)
		return false;

	fc_targoffset = fc_hscan->rs_vistuples[fc_hscan->rs_cindex];
	fc_dp = (Page) BufferGetPage(fc_hscan->rs_cbuf);
	fc_lp = PageGetItemId(fc_dp, fc_targoffset);
	Assert(ItemIdIsNormal(fc_lp));

	fc_hscan->rs_ctup.t_data = (HeapTupleHeader) PageGetItem((Page) fc_dp, fc_lp);
	fc_hscan->rs_ctup.t_len = ItemIdGetLength(fc_lp);
	fc_hscan->rs_ctup.t_tableOid = fc_scan->rs_rd->rd_id;
	ItemPointerSet(&fc_hscan->rs_ctup.t_self, fc_hscan->rs_cblock, fc_targoffset);

	pgstat_count_heap_fetch(fc_scan->rs_rd);

	/*
	 * 设置结果槽以指向此元组。 注意，这个槽在缓冲区上获得了一个引脚。
	 */
	ExecStoreBufferHeapTuple(&fc_hscan->rs_ctup,
							 fc_slot,
							 fc_hscan->rs_cbuf);

	fc_hscan->rs_cindex++;

	return true;
}

static bool fc_heapam_scan_sample_next_block(TableScanDesc fc_scan, SampleScanState *fc_scanstate)
{
	HeapScanDesc fc_hscan = (HeapScanDesc) fc_scan;
	TsmRoutine *fc_tsm = fc_scanstate->tsmroutine;
	BlockNumber fc_blockno;

	/* 如果关系为空，则立即返回 false */
	if (fc_hscan->rs_nblocks == 0)
		return false;

	if (fc_tsm->NextSampleBlock)
	{
		fc_blockno = fc_tsm->NextSampleBlock(fc_scanstate, fc_hscan->rs_nblocks);
		fc_hscan->rs_cblock = fc_blockno;
	}
	else
	{
		/* 顺序扫描表 */

		if (fc_hscan->rs_cblock == InvalidBlockNumber)
		{
			Assert(!fc_hscan->rs_inited);
			fc_blockno = fc_hscan->rs_startblock;
		}
		else
		{
			Assert(fc_hscan->rs_inited);

			fc_blockno = fc_hscan->rs_cblock + 1;

			if (fc_blockno >= fc_hscan->rs_nblocks)
			{
				/* 回到关系的开始，可能没有从 0 开始 */
				fc_blockno = 0;
			}

			/*
			 * 报告我们的新扫描位置以进行同步。
			 *
			 * 注意：我们在检查扫描结束之前执行此操作，以便位置提示的最终状态回到关系的开始。这并不是严格必要的，但在多次运行相同查询时，起始位置会在每次调用时向后移动一点，这会造成困惑。尽管如此，我们通常不保证任何特定的顺序。
			 */
			if (fc_scan->rs_flags & SO_ALLOW_SYNC)
				ss_report_location(fc_scan->rs_rd, fc_blockno);

			if (fc_blockno == fc_hscan->rs_startblock)
			{
				fc_blockno = InvalidBlockNumber;
			}
		}
	}

	if (!BlockNumberIsValid(fc_blockno))
	{
		if (BufferIsValid(fc_hscan->rs_cbuf))
			ReleaseBuffer(fc_hscan->rs_cbuf);
		fc_hscan->rs_cbuf = InvalidBuffer;
		fc_hscan->rs_cblock = InvalidBlockNumber;
		fc_hscan->rs_inited = false;

		return false;
	}

	heapgetpage(fc_scan, fc_blockno);
	fc_hscan->rs_inited = true;

	return true;
}

static bool fc_heapam_scan_sample_next_tuple(TableScanDesc fc_scan, SampleScanState *fc_scanstate,
							  TupleTableSlot *fc_slot)
{
	HeapScanDesc fc_hscan = (HeapScanDesc) fc_scan;
	TsmRoutine *fc_tsm = fc_scanstate->tsmroutine;
	BlockNumber fc_blockno = fc_hscan->rs_cblock;
	bool		fc_pagemode = (fc_scan->rs_flags & SO_ALLOW_PAGEMODE) != 0;

	Page		fc_page;
	bool		fc_all_visible;
	OffsetNumber fc_maxoffset;

	/*
	 * 当不使用页面模式时，我们必须在元组可见性检查期间锁定缓冲区。
	 */
	if (!fc_pagemode)
		LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_SHARE);

	fc_page = (Page) BufferGetPage(fc_hscan->rs_cbuf);
	fc_all_visible = PageIsAllVisible(fc_page) &&
		!fc_scan->rs_snapshot->takenDuringRecovery;
	fc_maxoffset = PageGetMaxOffsetNumber(fc_page);

	for (;;)
	{
		OffsetNumber fc_tupoffset;

		CHECK_FOR_INTERRUPTS();

		/* 询问表样本方法在此页面上检查哪些元组。 */
		fc_tupoffset = fc_tsm->NextSampleTuple(fc_scanstate,
										 fc_blockno,
										 fc_maxoffset);

		if (OffsetNumberIsValid(fc_tupoffset))
		{
			ItemId		fc_itemid;
			bool		fc_visible;
			HeapTuple	fc_tuple = &(fc_hscan->rs_ctup);

			/* 跳过无效的元组指针。 */
			fc_itemid = PageGetItemId(fc_page, fc_tupoffset);
			if (!ItemIdIsNormal(fc_itemid))
				continue;

			fc_tuple->t_data = (HeapTupleHeader) PageGetItem(fc_page, fc_itemid);
			fc_tuple->t_len = ItemIdGetLength(fc_itemid);
			ItemPointerSet(&(fc_tuple->t_self), fc_blockno, fc_tupoffset);


			if (fc_all_visible)
				fc_visible = true;
			else
				fc_visible = fc_SampleHeapTupleVisible(fc_scan, fc_hscan->rs_cbuf,
												 fc_tuple, fc_tupoffset);

			/* 在页面模式中，heapgetpage 为我们完成了这项工作 */
			if (!fc_pagemode)
				HeapCheckForSerializableConflictOut(fc_visible, fc_scan->rs_rd, fc_tuple,
													fc_hscan->rs_cbuf, fc_scan->rs_snapshot);

			/* 尝试从同一页面的下一个元组。 */
			if (!fc_visible)
				continue;

			/* 找到可见元组，返回它。 */
			if (!fc_pagemode)
				LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_UNLOCK);

			ExecStoreBufferHeapTuple(fc_tuple, fc_slot, fc_hscan->rs_cbuf);

			/* 将成功提取的元组计数为堆提取 */
			pgstat_count_heap_getnext(fc_scan->rs_rd);

			return true;
		}
		else
		{
			/*
			 * 如果我们到这里，意味着我们已耗尽了此页面上的项，现在是时候移动到下一个。
			 */
			if (!fc_pagemode)
				LockBuffer(fc_hscan->rs_cbuf, BUFFER_LOCK_UNLOCK);

			ExecClearTuple(fc_slot);
			return false;
		}
	}

	Assert(0);
}


/* ----------------------------------------------------------------------------
 *  上述的辅助函数。
 * ----------------------------------------------------------------------------
 */

/*
 * 重建和重写给定的元组
 *
 * 我们不能简单地按原样复制元组，原因有几个：
 *
 * 1. 我们希望挤出任何被丢弃列的值，既为了节省空间，也为了确保我们没有边缘案例故障。（例如，新表可能没有 TOAST 表，因此无法存储任何大值的丢弃列。）
 *
 * 2. 元组甚至可能对新表来说不合法；这目前仅在 ALTER TABLE SET WITHOUT OIDS 的后果中出现。
 *
 * 因此，我们必须从组件 Datums 重建元组。
 */
static void fc_reform_and_rewrite_tuple(HeapTuple fc_tuple,
						 Relation fc_OldHeap, Relation fc_NewHeap,
						 Datum *fc_values, bool *fc_isnull, RewriteState fc_rwstate)
{
	TupleDesc	fc_oldTupDesc = RelationGetDescr(fc_OldHeap);
	TupleDesc	fc_newTupDesc = RelationGetDescr(fc_NewHeap);
	HeapTuple	fc_copiedTuple;
	int			fc_i;

	heap_deform_tuple(fc_tuple, fc_oldTupDesc, fc_values, fc_isnull);

	/* 确保将任何丢弃的列设为 null */
	for (fc_i = 0; fc_i < fc_newTupDesc->natts; fc_i++)
	{
		if (TupleDescAttr(fc_newTupDesc, fc_i)->attisdropped)
			fc_isnull[fc_i] = true;
	}

	fc_copiedTuple = heap_form_tuple(fc_newTupDesc, fc_values, fc_isnull);

	/* 堆重写模块会处理其余的事情 */
	rewrite_heap_tuple(fc_rwstate, fc_tuple, fc_copiedTuple);

	heap_freetuple(fc_copiedTuple);
}

/*
 * 检查元组的可见性。
 */
static bool fc_SampleHeapTupleVisible(TableScanDesc fc_scan, Buffer fc_buffer,
					   HeapTuple fc_tuple,
					   OffsetNumber fc_tupoffset)
{
	HeapScanDesc fc_hscan = (HeapScanDesc) fc_scan;

	if (fc_scan->rs_flags & SO_ALLOW_PAGEMODE)
	{
		/*
		 * 在 pageatatime 模式下，heapgetpage() 已经进行了可见性检查，
		 * 所以只需查看它在 rs_vistuples[] 中留下的信息。
		 *
		 * 我们对已知排序数组进行二分搜索。注意：如果我们坚持让 NextSampleTuple 选择以递增顺序的元组，
		 * 我们可以节省一些工作，但并不清楚是否有足够的收益来证明这种限制的合理性。
		 */
		int			fc_start = 0,
					fc_end = fc_hscan->rs_ntuples - 1;

		while (fc_start <= fc_end)
		{
			int			fc_mid = (fc_start + fc_end) / 2;
			OffsetNumber fc_curoffset = fc_hscan->rs_vistuples[fc_mid];

			if (fc_tupoffset == fc_curoffset)
				return true;
			else if (fc_tupoffset < fc_curoffset)
				fc_end = fc_mid - 1;
			else
				fc_start = fc_mid + 1;
		}

		return false;
	}
	else
	{
		/* 否则，我们必须单独检查元组。 */
		return HeapTupleSatisfiesVisibility(fc_tuple, fc_scan->rs_snapshot,
											fc_buffer);
	}
}


/* ------------------------------------------------------------------------
 * 堆表访问方法的定义。
 * ------------------------------------------------------------------------
 */

static const TableAmRoutine heapam_methods = {
	.type = T_TableAmRoutine,

	.slot_callbacks = fc_heapam_slot_callbacks,

	.scan_begin = heap_beginscan,
	.scan_end = heap_endscan,
	.scan_rescan = heap_rescan,
	.scan_getnextslot = heap_getnextslot,

	.scan_set_tidrange = heap_set_tidrange,
	.scan_getnextslot_tidrange = heap_getnextslot_tidrange,

	.parallelscan_estimate = table_block_parallelscan_estimate,
	.parallelscan_initialize = table_block_parallelscan_initialize,
	.parallelscan_reinitialize = table_block_parallelscan_reinitialize,

	.index_fetch_begin = fc_heapam_index_fetch_begin,
	.index_fetch_reset = fc_heapam_index_fetch_reset,
	.index_fetch_end = fc_heapam_index_fetch_end,
	.index_fetch_tuple = fc_heapam_index_fetch_tuple,

	.tuple_insert = fc_heapam_tuple_insert,
	.tuple_insert_speculative = fc_heapam_tuple_insert_speculative,
	.tuple_complete_speculative = fc_heapam_tuple_complete_speculative,
	.multi_insert = heap_multi_insert,
	.tuple_delete = fc_heapam_tuple_delete,
	.tuple_update = fc_heapam_tuple_update,
	.tuple_lock = fc_heapam_tuple_lock,

	.tuple_fetch_row_version = fc_heapam_fetch_row_version,
	.tuple_get_latest_tid = heap_get_latest_tid,
	.tuple_tid_valid = fc_heapam_tuple_tid_valid,
	.tuple_satisfies_snapshot = fc_heapam_tuple_satisfies_snapshot,
	.index_delete_tuples = heap_index_delete_tuples,

	.relation_set_new_filenode = fc_heapam_relation_set_new_filenode,
	.relation_nontransactional_truncate = fc_heapam_relation_nontransactional_truncate,
	.relation_copy_data = fc_heapam_relation_copy_data,
	.relation_copy_for_cluster = fc_heapam_relation_copy_for_cluster,
	.relation_vacuum = heap_vacuum_rel,
	.scan_analyze_next_block = fc_heapam_scan_analyze_next_block,
	.scan_analyze_next_tuple = fc_heapam_scan_analyze_next_tuple,
	.index_build_range_scan = fc_heapam_index_build_range_scan,
	.index_validate_scan = fc_heapam_index_validate_scan,

	.relation_size = table_block_relation_size,
	.relation_needs_toast_table = fc_heapam_relation_needs_toast_table,
	.relation_toast_am = fc_heapam_relation_toast_am,
	.relation_fetch_toast_slice = heap_fetch_toast_slice,

	.relation_estimate_size = fc_heapam_estimate_rel_size,

	.scan_bitmap_next_block = fc_heapam_scan_bitmap_next_block,
	.scan_bitmap_next_tuple = fc_heapam_scan_bitmap_next_tuple,
	.scan_sample_next_block = fc_heapam_scan_sample_next_block,
	.scan_sample_next_tuple = fc_heapam_scan_sample_next_tuple
};


const TableAmRoutine *
GetHeapamTableAmRoutine(void)
{
	return &heapam_methods;
}

Datum heap_tableam_handler(PG_FUNCTION_ARGS)
{
	PG_RETURN_POINTER(&heapam_methods);
}
